函数
函数
函数:是组织好的,可重复使用的,用来实现特定功能的代码段。
我们使用过的:input()、print()、str()、int()、hex()、abs() 等都是 Python 的内置函数
定义函数
def function_name(params):
function_body
return value
其中参数可以省略,返回值也可以省略
函数使用步骤:先定义函数,后调用函数
简单实践:
# 可以直接在参数上设置默认值
def main(name="xiashuo.xyz"):
print(f"{name} main function")
return name + "-----"
# 因为python代码都是顺序执行的,我们调用函数之前,必须要保证这个函数已经定义了才可以,所以,对main函数的定义必须在对main函数的调用之前,否则会报错
# 使用参数的默认值
main()
main("aaa")
print(main("bbb"))
输出:
xiashuo.xyz main function
aaa main function
bbb main function
bbb-----
在 Java 中,定义一个方法,必须在类之中,其次,定义一个方法的时候无法在参数中指定默认值,跟 Python 一样,也可以没有返回值(返回值类型为 void),同时还有可见性的配置(private、public 等),Python 中好像没有可见性的配置
注意,如果变量跟方法同名,则会相互覆盖,谁后声明,谁最后生效。所以,我们定义变量/方法的时候,千万要注意不要跟已经声明的方法/变量同名。不然会造成麻烦
当变量跟方法重名了的时候,Pycharm 一般都会给个提示,要么是标黄,要么是底下有下划线
简单测试如下:
# 注意,如果变量跟方法同名,则会相互覆盖,谁后声明,谁最后生效
# 所以,我们定义变量/方法的时候,千万要注意不要跟已经声明的方法/变量同名
# 不然会造成麻烦
def test_func(aaa, bbb):
print(f"{aaa},{bbb}")
return aaa
test_func = "124546"
print(test_func)
# test_func("sdsd", "sdsd")
my_name = "xiashuo"
def my_name():
print("xiashuo")
# 输出 <function my_name at 0x0000017EBC33A830>
print(my_name)
# 输出 xiashuo
my_name()
输出:
124546
<function my_name at 0x0000017EBC33A830>
xiashuo
添加方法注释
方法体的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
pyCharm中添加方法注释(Docstring format & Live Templates)_pycharm自动添加方法注释_dkjkls的博客-CSDN博客
在方法体的第一行中输入 """ 或者 ''' 然后回车,自动生成 reStructuredText 风格的注释,
注释的风格可以在
File -> Settings -> Tools -> Python Integrated Tools -> Docstrings -> Docstring format中切换
实践如下:
def test_doc(aaa,bbb):
"""
测试输出
:param aaa:
:param bbb:
:return:
"""
print(f"{aaa},{bbb}")
return aaa
test_doc(12, "sdsd")
将鼠标放到 test_doc 上,会有提示:
pass 关键字
在 Python 中有时候会看到一个 def 函数:
def sample(n_samples):
pass
该处的 pass 便是占据一个位置,因为如果定义一个空函数程序会报错,当你没有想好函数的内容是可以用 pass 填充,使程序可以正常运行。
以 pass 作为方法体的函数,有点类似于 Java 中的抽象类,可以调用,没有任何输出。相当于啥都不做。
别名
函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1
方法参数
调用方法时可使用的正式参数类型
-
位置参数(positional argument)
位置参数须以正确的顺序传入函数。调用时的数量必须和声明时的一样。
-
关键字参数
函数调用时通过“键=值”形式传递参数。使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值。
关键字参数只能在位置参数的后面
-
默认参数
相关实践请看
默认值小节 -
不定长参数
不定长参数也叫可变参数. 用于不确定调用的时候会传递多少个参数 (不传参也可以) 的场景,类型有,位置传递的不定长参数(以
*开头的参数)和关键字传递的不定长参数(以**开头的参数),相关实践请看参数名以*开头小节和参数名以**开头小节。
关于关键字参数,简单实践如下:
def info(age,name):
return print(f"我的名字是:{name},年龄是:{age}")
# 必需输入 name 和 age,否则报错
# TypeError: info() missing 2 required positional arguments: 'age' and 'name'
# info()
info(12,"xiashuo")
# 使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值。
info(name="xiashuo1212",age=20)
输出:
我的名字是:xiashuo,年龄是:12
我的名字是:xiashuo1212,年龄是:20
默认值
有了默认值,在调用函数的时候,就可以不传入这个参数。注意:带默认值的参数的后面不能存在不带默认值的参数,同时后面的参数可以声明为以 * 或者 ** 开头的参数或者 / 或者 *
def test_default_params(age, name="xiashuo.xyz", *dfad, **kw):
print(f"age: {age} name: {name} ")
# 默认参数值
test_default_params(10)
# 指定的参数值
test_default_params(20,'aaaa')
输出:
age: 10 name: xiashuo.xyz
age: 20 name: aaaa
当我们增加方法的参数的时候,可以为新增的参数设置一个默认值,这样旧的调用代码可以不传这个新的参数,这样就可以兼容旧的调用代码。
默认值需要设置成不变对象
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个 list,添加一个 END 再返回:
def add_end(L=[]):
L.append('END')
return L
此时编译器会标黄,提示 Default argument value is mutable
当你正常调用时,结果似乎不错:
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
当你使用默认参数调用时,一开始结果也是对的:
>>> add_end()
['END']
但是,再次调用 add_end() 时,结果就不对了:
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
很多初学者很疑惑,默认参数是 [],但是函数似乎每次都“记住了”上次添加了 'END' 后的 list。
原因解释如下:
Python 函数在定义的时候,默认参数 L 的值就被计算出来了,即 [],因为默认参数 L 也是一个变量,它指向对象 [],每次调用该函数,如果改变了 L 的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的 [] 了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用 None 这个不变对象来实现:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
现在,无论调用多少次,都不会有问题:
>>> add_end()
['END']
>>> add_end()
['END']
为什么要设计 str、None 这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
特殊类型的参数
参数名以 * 开头
将传入的多个参数以元组类型保存,前面的参数保证每一个都有值,剩下的都给以 * 开头的参数,同时后面的参数都必须以关键字参数的方式传
此时以 * 开头的参数在函数内部的类型是 tuple(不可更改)
# 将传入的多个参数以元组类型保存
def add(*num0):
sum = 0
for i in num0:
sum += i
return sum
print(add(12, 13, 15))
输出:
40
以 * 开头的参数前面,后面还有参数的情况
def test_astrisk_params(a, *b, c):
print(a, b, c)
# a 为 a
# b 为内容为 ('b', 'c', 'd', 'e', 'f', 'f') 的元组
# c 为 && 为关键字参数
print(test_astrisk_params(*"abcdef",c="&&"))
输出:
a ('b', 'c', 'd', 'e', 'f', 'f') &&
可以看到,前面的参数保证每一个都有值,剩下的都给以 * 开头的参数,同时后面的参数都必须以关键字参数的方式传。
如果你有一个 list,想要把它的元素当成一个支持可变参数的方法的参数传入,可以直接 用 *list,即 python解包。
参数名以 ** 开头
将传入的多个参数以字典类型保存,注意,传参的时候,键值对中的建不能是数字,以 ** 开头的参数后面不能再声明参数,因此,以 ** 开头的参数都是参数列表的最后一个参数。
# 将传入的多个参数以字典类型保存
def add_new(**num1):
sum = 0
for i in num1.values():
sum += i
return sum
# key 不能是数字
print(add_new(e=12, f=13, g=15))
输出:
40
同时使用以 * 开头的参数和以 ** 开头的参数
def test_double_astrisk_and_astrisk_params(a, *b, c, **d):
print(a, b, c, d)
# c 在 以`*`开头的参数的后面,因此,必须为关键字参数
# a 的值由字符串解包,为 "a"
test_double_astrisk_and_astrisk_params(*"abcde", c="fgh", f=13, g=15)
# a 的值由位置参数直接指定,为 10
test_double_astrisk_and_astrisk_params(10, *"abcde", c="fgh", f=13, g=15)
输出:
a ('b', 'c', 'd', 'e') fgh {'f': 13, 'g': 15}
10 ('a', 'b', 'c', 'd', 'e') fgh {'f': 13, 'g': 15}
关键字参数有什么用?它可以扩展函数的功能。比如,在 person 函数里,我们保证能接收到 name 和 age 这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
调用
>>> person('Michael', 30)
name: Michael age: 30 other: {}
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
注意,一个 dict 经过解包,然后再变成形参,已经解耦了,所以对 **arg 的修改不会影响原参数
def arg_test(**dict):
dict['name']=41
print(dict)
d={"age":51}
arg_test(**d)
print(d)
输出
{'age': 51, 'name': 41}
{'age': 51}
参数名为 *
参数为 *,如果方法的参数列表中单独出现星号 *,则星号 * 后的参数必须用关键字的格式传入,* 前面的不做限制
* 后面的参数被视为命名关键字参数(keyword-only arguments)。意思是名字固定的关键字参数。
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,我们写代码的时候根本就不知道,如果要限制关键字参数的名字,就可以用命名关键字参数。
这和以
*开头的参数的逻辑是一样的,
简单实践如下:
def test_asterisk(name, age, *, hobby):
return print(f"我的名字是:{name},年龄是:{age},爱好是{hobby}")
# TypeError: test_asterisk() takes 2 positional arguments but 3 were given
# test_asterisk("xiashuo", 45, "钓鱼")
test_asterisk("xiashuo", 45, hobby="钓鱼")
test_asterisk(age=60,name="xiashuo2", hobby="钓鱼")
def keywords(*, name, age):
return print(f"我的名字是:{name},年龄是:{age}")
keywords(name="xiashuo", age=12)
输出:
我的名字是:xiashuo,年龄是:45,爱好是钓鱼
我的名字是:xiashuo2,年龄是:60,爱好是钓鱼
我的名字是:xiashuo,年龄是:12
根据 5-函数 > 参数名以 * 开头 中提到的规则 前面的参数保证每一个都有值,剩下的都给以 * 开头的参数,同时后面的参数都必须以关键字参数的方式传,如果前面一个参数以 * 开头,这个以 * 开头的参数后面的参数,都是命名关键字参数
def person(name, age, *args, city, job):
print(name, age, args, city, job)
person('Jack', 24, *[1, 2, 3], city='Beijing', job='Engineer')
输出
Jack 24 (1, 2, 3) Beijing Engineer
参数为/
参数为 /,如果方法的参数列表中单独出现斜杠 /,则斜杠 / 前面的参数必须用位置参数的格式传入,/ 后面的不做要求
简单实践如下:
def positional_arguments(name, age, hobby, /, family):
return print(f"我的名字是:{name},年龄是:{age},爱好是{hobby},我的家庭是{family}")
# TypeError: positional_arguments() got some positional-only arguments passed as keyword arguments: 'name, age'
# positional_arguments("钓鱼",age=12, name="xiashuo",family="虾家")
positional_arguments("xiashuo", 12, "钓鱼", family="虾家")
positional_arguments("xiashuo3", 35, "钓鱼", "虾家")
输出:
我的名字是:xiashuo,年龄是:12,爱好是钓鱼,我的家庭是虾家
我的名字是:xiashuo3,年龄是:35,爱好是钓鱼,我的家庭是虾家
/ 和 * 混合使用,显然,/ 必须在 * 的前面,也必须在以 * 开头的参数前面
def f(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
f(10, 20, 30, d=40, e=50, f=60)
# f(10, b=20, c=30, d=40, e=50, f=60) # b 不能使用关键字参数的形式
# f(10, 20, 30, 40, 50, f=60) # e 必须使用关键字参数的形式
输出:
10 20 30 40 50 60
python 参数传递的灵活性
在 Python 中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这 5 种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
举个例子
def func_param(a,b,c=0,*l,m,**d):
print(a,b,c,l,m,d)
func_param(*[1,2,3,4,5],**{'m':1,'d':2})
输出
1 2 3 (4, 5) 1 {'d': 2}
而且如果一个函数定义为
def func(*args,**kw):
pass
那么你就可以任意传参了,即,传任何参数都不会出现传参失败的错误,
举个例子:
def func_param(*l, **d):
print(l, d)
这样调用
func_param(1, 2, 3, 4, 5)
func_param(a=1)
func_param(1, 2, 3, a=1, b=2)
func_param(*[1, 2, 3, 4, 5], **{'m': 1, 'd': 2})
func_param(1, 2, 3, 4, 5, m=1, d=2, **{"a": 2, "b": 5})
输出
(1, 2, 3, 4, 5) {}
() {'a': 1}
(1, 2, 3) {'a': 1, 'b': 2}
(1, 2, 3, 4, 5) {'m': 1, 'd': 2}
(1, 2, 3, 4, 5) {'m': 1, 'd': 2, 'a': 2, 'b': 5}
根据这一点,我们在创建方法但是还不确定具体参数的时候,可以先把这两个参数写上 func(*args,**kw),当我们确定方法参数了,我们就在正确的位置加上参数,比如 func(age,name,gender=1,*args,score,**kw) 。这也是很多框架、装饰器(Python 装饰器)、中间件、SDK 里常见的写法。
python 方法传参的本质
函数调用时,Python 不是按“参数长什么样”来调用函数,而是先把调用现场整理成一批“位置参数”和“关键字参数”,然后再根据函数定义的参数规则,把这些值分配到对应的参数名上。
函数调用的本质:先收集,再分配
例如:
func(1, 2, a=3, b=4)
Python 内部可以理解成:
位置参数 = (1, 2)
关键字参数 = {"a": 3, "b": 4}
而:
args = (1, 2)
kw = {"a": 3, "b": 4}
func(*args, **kw)
展开以后也是:
func(1, 2, a=3, b=4)
所以这两种写法本质上等价。
函数定义的本质:规定参数如何接收值
因为任何一次函数调用,最终都可以看成两部分:
位置参数 tuple
关键字参数 dict
例如:
f(1, 2, c=3)
可以等价写成:
args = (1, 2)
kw = {"c": 3}
f(*args, **kw)
再比如:
f(a=1, b=2, c=3)
可以写成:
args = ()
kw = {"a": 1, "b": 2, "c": 3}
f(*args, **kw)
所以从调用机制上看:
func(*args, **kw)
就是 Python 调用函数的“标准模型”。
这也是为什么我们把方法参数定义为
def func(*args, **kw):
pass
的时候,用户怎么传参都不会报错
Python 参数分配的顺序
大致可以理解成:
1. 先把调用端的 *args 展开成位置参数
2. 再把调用端的 **kw 展开成关键字参数
3. 位置参数按顺序绑定到函数定义中的参数
4. 关键字参数按名字绑定到对应参数
5. 多余的位置参数交给 *args
6. 多余的关键字参数交给 **kw
7. 没传的参数尝试使用默认值
8. 仍然缺少必填参数,就报错
9. 同一个参数被赋值多次,也报错
比如:
def f(a, b, c=3, *args, d, **kw):
pass
调用:
f(1, 2, 3, 4, 5, d=6, x=7)
绑定结果是:
a = 1
b = 2
c = 3
args = (4, 5)
d = 6
kw = {"x": 7}
最核心的理解
你可以把 Python 函数调用想象成这样:
def f(a, b, c=3, *args, d, **kw):
...
不是函数“主动去拿参数”,而是 Python 在调用时做了一次参数绑定:
调用现场:
位置参数:(1, 2, 3, 4)
关键字参数:{"d": 5, "x": 6}
函数签名:
a, b, c=3, *args, d, **kw
绑定结果:
a = 1
b = 2
c = 3
args = (4,)
d = 5
kw = {"x": 6}
所以参数分配的本质就是:
把调用时提供的“位置参数序列”和“关键字参数映射”,按照函数签名规则,绑定到函数内部的局部变量名上。
这就是 Python 方法参数分配的核心。
值传递和引用传递
在 python 中,类型属于对象,对象有不同类型的区分,变量是没有类型的:
a=[1,2,3]
a="Runoob"
以上代码中,[1,2,3] 是 List 类型,"Runoob" 是 String 类型,而变量 a 是没有类型,她仅仅是一个对象的引用(一个指针),可以是指向 List 类型对象,也可以是指向 String 类型对象。
可更改 (mutable) 与不可更改 (immutable) 对象
在 python 中,strings, tuples, 和 numbers(数字) 是不可更改的对象,而 list,set,dict 等则是可以修改的对象。
- 不可变类型:变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。
- 可变类型:变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身 la 没有动,只是其内部的一部分值被修改了。
python 函数的参数传递:
- 不可变类型:类似 C++ 的值传递,如整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。如果在 fun(a) 内部修改 a 的值,则是新生成一个 a 的对象。
- 可变类型:类似 C++ 的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响
python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。
参数类型检查
可以用 isinstance 进行参数类型检查
def my_func(age):
isint = isinstance(age, (int,str))
if not isint:
raise ValueError("age must be int or str")
else:
print("age:",age,"是 int 或者 str:",isint)
不过有一点需要注意,False 和 True 会被视为 int 类型
print(isinstance(False,int))
print(isinstance(True,int))
输出
True
返回值
return [表达式] 语句用于退出函数,选择性地向调用方返回一个表达式。不带参数值的 return 语句返回 None
简答实践如下
def add_num(*nums):
sum = 0
for i in nums:
sum += i
return sum
sum = add_num(12, 12, 45)
print(sum)
def test_none():
pass
# var 的值是 None ,类型是 NoneType
var = test_none()
print(var,type(var))
def test_none2():
# 也可以直接返回None
return None
输出:
69
None <class 'NoneType'>
变量作用域
变量作用域指的是变量的作用范围(变量在哪里可用,在哪里不可用)
主要分为两类:局部变量和全局变量
- 局部变量:定义在函数体内部的变量,即只在函数体内部生效
- 全局变量:在函数体内、外都能生效的变量,如果有一个数据,在函数 A 和函数 B 中都要使用,那么我们将这个数据存储在一个全局变量里面即可,
在函数体内部,可以直接读全局变量,但是不可以直接写全局变量,想要在函数体内写全局变量,需要在写之前通过 global varibale_name 声明一下,这个变量是全局变量。
说实话,这样不够优雅,有点像打补丁
此外还有一个问题,当我们在函数内部定义了一个跟全局变量同名的变量的时候,这个变量是局部变量还是全局变量呢?
默认是全局变量,可以通过添加 global varibale_name 声明将其声明为全局变量,
同时,我们也可以直接在函数内部通过 global varibale_name 声明一个新的全局变量,然后在别的函数中即可直接使用这个全局变量。
这样我们写代码的时候就更加灵活了,但是也可能会造成问题看下面的实践
简单总结就是,global 关键字的作用是让我们可以在函数内部声明一个变量为全局变量
global 提供了一种,在函数内部,定义全局变量的方式
简单实践如下:
num = 100
def test_1():
# 在函数内部是可以直接使用全局变量的
print(f"test_1:{num}")
def test_2():
# 在函数内部是可以直接使用全局变量的
print(f"test_2:{num}")
test_1()
test_2()
print(f"outer:{num}")
print("------------------------------------------------")
def test_3():
# 下面的语句提示 Unresolved reference 'num' ,说明这个num无法指向全局的num
# num += 50
# 此时这个num不是全局的num,而是 test_3 这个方法内部的局部变量 num
# 对这个局部变量的定义/修改不会影响到全局变量
num = 200
print(f"test_3:{num}")
test_1()
# test_3 中定义的是局部变量,不会影响外部的全局变量
test_3()
# 全局变量并没有变化
print(f"outer:{num}")
print("------------------------------------------------")
def test_4():
# 声明 num 就是全局变量
global num
num = 200
print(f"test_4:{num}")
test_1()
test_4()
# 全局变量同步了变化
print(f"outer:{num}")
print("------------------------------------------------")
# global 提供了一种,在函数内部,定义全局变量的方式,这样我们写代码的时候就更加灵活了
def test_5():
# 声明 age 就是全局变量
global age
age = 600
print(f"test_5:{age}")
def test_6():
# 直接使用 age 这个全局变量
print(f"test_6:{age}")
# 此时 test_5 必须在 test_6 的前面被调用
# 可能会有问题
test_5()
test_6()
print(f"outer:{age}")
输出:
test_1:100
test_2:100
outer:100
------------------------------------------------
test_1:100
test_3:200
outer:100
------------------------------------------------
test_1:100
test_4:200
outer:200
------------------------------------------------
test_5:600
test_6:600
outer:600
在 Java 中,这个问题可以用 this 关键字解决
函数的多返回值
本质上,使用的还是元组可以省略圆括号和 Python 自动解包的特性,具体请看《Python 解包.md》
这个跟 for 循环同时返回多个变量有异曲同工之妙
解包的功能只是把元素从容器中拿出来,但是在赋值语句中,实现了多变量同时赋值,这个特性跟 for 组合实现了 for 循环输出多个值,跟 return 组合实现了方法同时返回多个值
程序之美就是这样,增加一个简单的概念,经过和现有的概念进行组合,可以实现非常复杂的新特性
简单实践如下:
def test_multiple_return_val(x, y, z):
return x, y, z
x, y, z = test_multiple_return_val(100, True, (1,2,3))
print(x, y, z)
输出:
100 True (1, 2, 3)
函数作为参数传递 - 面向函数编程
将一个函数当作一个参数进行传递,是面向函数编程的思想
在 Java 中,lambda 表达式在使用的时候是有类型限制的(函数式接口),即你传入的函数必须符合某种格式,这其实是有所限制的,灵活度不够大,
但是在 Python 中,灵活度非常大,你可以传入任意格式的函数,几乎没有什么限制
函数式编程代表的是一种新的抽象和思考的方式
函数式编程关心数据的映射,命令式编程关心解决问题的步骤
什么是函数式编程思维? - nameoverflow 的回答 - 知乎
https://www.zhihu.com/question/28292740/answer/100284611
什么是函数式编程思维? - 用心阁的回答 - 知乎
https://www.zhihu.com/question/28292740/answer/40336090
在前面的函数学习中,我们一直使用的函数,都是接受数据作为参数传入:
- 数字
- 字符串
- 字典、列表、元组等
其实,我们学习的函数本身,也可以作为参数传入另一个函数内。
简单实践,在下面的例子中,operate 未声明类型,可以接收数据,也可以接收函数(类型为 function),而且是可以接受任何类型的参数,没有任何限制,这个非常牛逼,
这个跟 Java 不一样,Java 在使用函数类型的参数(lambda 表达式)的时候是需要声明类型的(函数式接口)
但是这样,调用方也可能传入一个完全不相关的不满足要求的函数,在传入这个不满足要求的函数的时候不会报错的,所以我们在执行传入的方法的时候完全不能保证传入的方法是不是我们想要的方法,所以最好加上 try-except
def calculate_num(operate, num0, num1, /):
# 类型为 function
# print(operate,type(operate))
result = -1
try:
result = operate(num0, num1)
except :
print("calculate_num error")
return result
def add(num0, num1):
return num0 + num1
def minus(num0, num1):
return num0 - num1
def not_relevent():
return "------"
# 传入加运算
print(calculate_num(add, 1, 2))
# 传入减运算
print(calculate_num(minus, 10, 2))
# 输出
# calculate_num error
# -1
print(calculate_num(not_relevent, 10, 2))
输出:
3
8
calculate_num error
-1
20
5.0
lambda 匿名函数
Python 使用 lambda 来创建匿名函数。
所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。
- lambda 只是一个表达式,函数体比 def 简单很多。
- lambda 的主体是一个表达式,而不是一个代码块。仅仅能在 lambda 表达式中封装有限的逻辑进去。
- lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
- 虽然 lambda 函数看起来只能写一行,却不等同于 C 或 C++ 的内联函数,后者的目的是调用小函数时不占用栈内存从而增加运行效率。
语法
lambda 函数的语法只包含一个语句,如下:
lambda [arg1 [,arg2,.....argn]]:expression
lambda 函数可以用变量承接,变量类型为 function
这一点跟 Java 是一样的
func_lambda = lambda num0, num1: num0 * num1
# 输出 <function <lambda> at 0x000002A3148E9120> <class 'function'>
print(func_lambda, type(func_lambda))
可以省略参数
no_param = lambda : 1
print(no_param, type(no_param))
在调用上文的 calculate_num 方法的时候,同样可以直接传入 lambda 表达式来作为函数类型的参数
# 输出 20
print(calculate_num(lambda num0, num1: num0 * num1, 10, 2))
# 输出 5.0
print(calculate_num(lambda num0, num1: num0 / num1, 10, 2))
我们可以将匿名函数封装在一个函数内,这样可以使用同样的代码来创建多个匿名函数。
以下实例将匿名函数封装在 myfunc 函数中,通过传入不同的参数来创建不同的匿名函数:
def myfunc(n):
return lambda a : a * n
mydoubler = myfunc(2)
mytripler = myfunc(3)
print(mydoubler(11))
print(mytripler(11))
输出:
22
33
python 的函数没有重载
在Python 里,函数“没有传统意义上的重载(overload)”。
也就是说: 你不能像 Java / C++ 那样写多个同名函数,根据参数列表自动选择执行哪个。
Python 不支持这种写法
def add(a, b):
return a + b
def add(a, b, c):
return a + b + c
👉 后定义的会直接覆盖前面的
add(1, 2) # ❌ 会报错,因为只剩 add(a, b, c)
Python 为什么不支持重载?
因为 Python 的函数名本质是一个变量:
add = function_object_1
add = function_object_2 # 直接覆盖
所以同名函数不会“共存”。
让函数跟一个变量一样可以随意传参,带来了极致的灵活性,但是也带来了另一个方面的损失,不过函数参数以及足够灵活,可以弥补多态的需求。
Python 替代方案
默认参数(最常用)
def add(a, b, c=0):
return a + b + c
add(1, 2) # 3
add(1, 2, 3) # 6
可变参数 *args(灵活)
def add(*args):
return sum(args)
add(1, 2)
add(1, 2, 3, 4)
手动类型判断(Python 风格“重载”)
def add(a, b):
if isinstance(b, list):
return a + sum(b)
return a + b
functools.singledispatch(真正“函数重载”的模拟)
适用于按类型分发
from functools import singledispatch
@singledispatch
def show(x):
print("default:", x)
@show.register
def _(x: int):
print("int:", x)
@show.register
def _(x: str):
print("str:", x)
show(10) # int
show("hi") # str
typing.overload(⚠️ 只给类型检查器用)
from typing import overload
@overload
def add(a: int, b: int) -> int: ...
@overload
def add(a: int, b: int, c: int) -> int: ...
def add(*args):
return sum(args)
👉 注意:
- 只是给 mypy / pyright / IDE 提示用
- 运行时不会生效
总结一句话
Python 没有真正的函数重载,只有“参数灵活 + 类型分发 + 手动逻辑”来模拟。